<?php

namespace Respect\StackedModularDispatcher;

use SplObjectStorage;
use ArrayAccess;
use ArrayObject;
use Closure;

class Dispatcher
{

    protected $instanceList;
    protected $moduleLoader;
    protected $dataContainer;

    public function __construct($dataContainer, $moduleLoader)
    {
        $this->instanceList = new ArrayObject();
        $this->registerDataContainer($dataContainer);
        $this->registerModuleLoader($moduleLoader);
    }

    public function appendInstance($instance)
    {
        if (!is_object($instance)) {
            $instance = new $instance;
        }
        $this->instanceList[] = $instance;
        //make the first instance go always on bottom
        $this->instanceList->uksort(
            function ($a, $b) {
                if ($a === 0)
                    return 1;
                if ($b === 0)
                    return - 1;
                return $a >= $b;
            }
        );
    }

    public function appendInstances(array $instances)
    {
        foreach ($instances as $instance) {
            $this->appendInstance($instance);
        }
    }

    public function replaceInstance($oldInstance, $newInstance)
    {
        if ($oldInstance === $newInstance)
            return;
        foreach ($this->instanceList as $k => $instance) {
            if ($instance === $oldInstance) {
                $this->instanceList[$k] = $newInstance;
            }
        }
    }

    protected function registerDataContainer($dataLoader)
    {
        $this->dataContainer = $dataLoader;
    }

    protected function registerModuleLoader($moduleLoader)
    {
        $this->moduleLoader = $moduleLoader;
    }

    public function loadModule($moduleName)
    {
        $loader = $this->moduleLoader;
        return $loader($moduleName);
    }

    public function run()
    {
        $this->runAll('setup');
        $this->handle($this);
        $this->runAll('finish', true);
    }

    protected function extractModuleNames($methodName)
    {
        return explode(' ', preg_replace('#[A-Z]+#', ' $0', $methodName));
    }

    protected function runAll($type, $reverse = false)
    {
        $target = $reverse ? array_reverse($this->instanceList->getArrayCopy()) : $this->instanceList;
        foreach ($target as $instance) {
            $this->runMethods($type, $instance);
        }
    }

    protected function runMethods($type, $instance)
    {
        $data = $this->dataContainer;
        $methodsArguments = $this->parseMethods($instance);
        foreach ($methodsArguments as $names) {
            if ($names[0] !== $type)
                continue;
            $arguments = $this->getArguments($names);
            if ($this->validateMethod($arguments)) {
                $methodResponse = $this->callMethod(
                        $instance,
                        $names,
                        $arguments,
                        $data
                );
            }
            if (isset($methodResponse)) {
                if (is_object($methodResponse)) {
                    $this->replaceInstance($instance, $methodResponse);
                } elseif (is_array($methodResponse)) {
                    $this->appendInstances($methodResponse);
                } 
            }
        }
    }

    protected function parseMethods($instance)
    {
        $methodsNames = get_class_methods($instance);
        return array_map(array($this, 'extractModuleNames'), $methodsNames);
    }

    protected function validateMethod($arguments)
    {
        return (!in_array(false, $arguments, true));
    }

    protected function getArguments($names)
    {
        return array_map($this->moduleLoader, array_slice($names, 1));
    }

    protected function callMethod($instance, $names, $arguments, $extraParameter)
    {
        array_unshift($arguments, $extraParameter);
        ob_start();
        $controllerResponse = call_user_func_array(
                array($instance, implode($names)),
                $arguments
        );
        ob_end_clean();
        return $controllerResponse;
    }

    protected function handle()
    {
        $complexHandle = null;
        $stackCopy = $this->instanceList->getArrayCopy();
        $instance = end($stackCopy);
        if (!$instance)
            return;
        $methodsArguments = $this->parseMethods($instance);
        foreach ($methodsArguments as $names) {
            if ($names[0] !== 'handle')
                continue;
            if (!is_null($complexHandle)
                &&
                count($complexHandle) > count($names))
                continue;
            $arguments = $this->getArguments($names);
            if (!$this->validateMethod($arguments))
                continue;
            $complexHandle = $names;
        }
        if (is_null($complexHandle))
            return;
        $handleResponse = $this->callMethod(
                $instance,
                $complexHandle,
                $arguments,
                $this->dataContainer
        );
        if (is_object($handleResponse)) {
            $this->replaceInstance($instance, $handleResponse);
            $instance = $handleResponse;
            $this->runMethods('setup', $instance);
            $this->handle($this);
        }
    }

    public function getInstances()
    {
        return array_values($this->instanceList->getArrayCopy());
    }

}
